#region File Header
//
// WavFileWriter.cs - Writes a mono WAV file. Supports different sample formats:
//
// Copyright (C) Javier Valcarce
// BSD License
#endregion

#region Using Statements
using System;
using System.Runtime.InteropServices;
using System.Text;
using System.IO;
#endregion

namespace Dsp
{
    public enum WavFormat
    {
        /// <summary>08-bit Linear Unsigned Integer PCM</summary>
        PCM08,
        /// <summary>16-bit Linear Signed Integer PCM</summary>
        PCM16,
        /// <summary>32-bit Linear Signed Integer PCM</summary>
        PCM24,
        /// <summary>32-bit Floating Point IEEE 754</summary>
        FLT32,
        /// <summary>Unknown</summary>
        UNKNOWN
    }


    /// <summary>
    /// Writes a *mono* WAV file. Supports different sample formats:
    /// * PCM08: 08-bit Linear PCM
    /// * PCM16: 16-bit Linear PCM
    /// * PCM32: 24-bit Linear PCM
    /// * FLT32: 32-bit Floating Point (IEEE 754)
    /// 
    /// In the case of PCM format, the input samples are clipped betwen [-1, +1) prior
    /// to convert them in 8, 16 or 24 bit integer data types. Therefore the precision 
    /// in PCM formats get larger with number of bits but the dynamic range is limited 
    /// between [-1, +1). If you want to have to record signals with a greater dynamic 
    /// range and better SNR use FLT32 format.
    /// 
    /// References:
    /// http://www.sonicspot.com/guide/wavefiles.html
    /// </summary>
    public class WavFileWriter : IDisposable
    {
        private WavFormat    _format;
        private int          _fs;
        private BinaryWriter _writer;             // stream writer
        private uint         _sampleCount;
        private bool         _clipped;            // Any sample was clipped? (PCM formats)
        private bool         _disposed;

        // WAV File Header, 44 bytes
        private byte[]       _hdr_chunkID;        // "riff" little-endian!
        private uint         _hdr_chunkSize;      //
        private byte[]       _hdr_format;         // "wave" little-endian!
        private byte[]       _hdr_subchunk1ID;    // "fmt " little-endian!
        private uint         _hdr_subchunk1Size;
        private ushort       _hdr_audioFormat;
        private ushort       _hdr_numChannels;
        private uint         _hdr_sampleRate;
        private uint         _hdr_byteRate;
        private ushort       _hdr_blockAlign;
        private ushort       _hdr_bitsPerSample;
        private byte[]       _hdr_subchunk2ID;    // "data" little-endian!
        private uint         _hdr_subchunk2Size;
        

        #region Constructors
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="filename">File name created wiwth FileMode.Create (OVERWRITE MODE!)</param>
        /// <param name="format">Sample format</param>
        /// <param name="fs">Sampling frequency</param>
        public WavFileWriter(string filename, WavFormat format, int fs)           
        {
            // FileMode.Create    => Overwrite!
            // FileMode.CreateNew => Don't overwrite!
            FileStream  _stream = new FileStream(filename, FileMode.Create);
            Init(_stream, format, fs);
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="stream">Any Stream Class derived object with the property CanSeek set to true
        /// like files and memory streams but it isn't valid sockets, serial ports or the like streams</param>
        /// <param name="format">Sample format</param>
        /// <param name="fs">Sampling frequency</param>
        public WavFileWriter(Stream stream, WavFormat format, int fs)           
        {
            Init(stream, format, fs);
        }
        
        private void Init(Stream stream, WavFormat format, int fs)
        {
            _format = format;
            _fs = fs;
            _sampleCount = 0;
            _disposed = false;
            _clipped = false;
            _writer = new BinaryWriter(stream);

            // Make space for WAV file (44 bytes), written later...
            _writer.Seek(44, SeekOrigin.Begin);
        }
        #endregion

        
        /// <summary>
        /// Writes audio samples in the WAV file. If the sample format is PCM then a clip 
        /// operation between (+1, -1) must be performed because samples outside this range
        /// can't be represented with this fixed point format.
        /// </summary>
        /// <param name="buffer">Array with data</param>
        /// <param name="offset">Offset in the array</param>
        /// <param name="count">Number of samples that will be written</param>
        public int XferData(double[] buffer, int offset, int count)
        {
            if (_disposed)
            {
                throw new ObjectDisposedException("WavFileWriter");
            }

            int i;

            // Audio data must be between [-1, +1] for PCM formats. If not, must be clipped
            // thus, in PCM format, the dynamic range in limited to [-1, +1)
            if ((_format == WavFormat.PCM08) || 
                (_format == WavFormat.PCM16) || 
                (_format == WavFormat.PCM24))
            {
                for (i = offset; i < offset + count; i++)
                {
                    if (buffer[i] > +1.0) { buffer[i] = +1.0; _clipped = true; }
                    if (buffer[i] < -1.0) { buffer[i] = -1.0; _clipped = true; }
                }
            }

            // samples in different formats
            byte  s08;
            short s16;
            int   s24;
            byte  s24_1;
            byte  s24_2;
            byte  s24_3;
            float s32;

            int items = 0;

            for (i = offset; i < offset + count; i++)
            {
                switch (_format)
                {
                    case WavFormat.PCM08: 
                        s08 = (byte) (((1 + buffer[i]) / 2) * 255); 
                        _writer.Write(s08); 
                        break;

                    case WavFormat.PCM16: 
                        s16 = (short) (buffer[i] * 0032767); 
                        _writer.Write(s16); 
                        break;

                    case WavFormat.PCM24:
                        // only LSB 24-bit take effect!
                        s24   = (int)  (buffer[i] * 8388607); 

                        s24_1 = (byte) ((s24 & 0x000000FF)      );
                        s24_2 = (byte) ((s24 & 0x0000FF00) >> 08);
                        s24_3 = (byte) ((s24 & 0x00FF0000) >> 16);
                        
                        // WAV File Format => little endian!
                        _writer.Write(s24_1);
                        _writer.Write(s24_2);
                        _writer.Write(s24_3);
                        break;

                    case WavFormat.FLT32: 
                        s32 = (float)  (buffer[i]); 
                        _writer.Write(s32); 
                        break;
                }

                _sampleCount++;
                items++;
            }

            return items;
        }

        #region Properties
        public bool      Closed       { get { return _disposed;          } }
        public bool      Clipped      { get { return _clipped;           } }
        public Stream    BaseStream   { get { return _writer.BaseStream; } }
        public WavFormat SampleFormat { get { return _format;            } }
        public int       Fs           { get { return _fs;                } }
        #endregion

        /// <summary>
        /// The same as Dispose()
        /// </summary>
        public void Close()
        {
            Dispose();
        }


        #region Miembros de IDisposable
        /// <summary>
        /// Finish writting the file header to achieve a coherent WAV header and then close the 
        /// opened stream (file). Not thread-safe.
        /// </summary>
        public void Dispose()
        {                       
            // WAV file header fields
            switch (_format)
            {
                case WavFormat.PCM08:
                    // 8-bit samples are always stores as UNSIGNED (centered at 128)
                    _hdr_audioFormat = (ushort)0x0001;
                    _hdr_bitsPerSample = (ushort)08;
                    break;
                case WavFormat.PCM16:
                    _hdr_audioFormat = (ushort)0x0001;
                    _hdr_bitsPerSample = (ushort)16;
                    break;

                case WavFormat.PCM24:
                    _hdr_audioFormat = (ushort)0x0001;
                    _hdr_bitsPerSample = (ushort)24;
                    break;

                case WavFormat.FLT32:
                    _hdr_audioFormat = (ushort)0x0003;
                    _hdr_bitsPerSample = (ushort)32;
                    break;
            }


            ASCIIEncoding ascii = new ASCIIEncoding();

            _hdr_chunkID = ascii.GetBytes("RIFF");
            _hdr_format = ascii.GetBytes("WAVE");
            _hdr_subchunk1ID = ascii.GetBytes("fmt ");
            _hdr_subchunk2ID = ascii.GetBytes("data");
            _hdr_subchunk1Size = 16;
            _hdr_numChannels = 1;
            _hdr_sampleRate = (uint)_fs;
            _hdr_byteRate = _hdr_sampleRate * _hdr_numChannels * _hdr_bitsPerSample / 8;
            _hdr_blockAlign = (ushort)(_hdr_numChannels * _hdr_bitsPerSample / 8);
            _hdr_subchunk2Size = _sampleCount * _hdr_numChannels * _hdr_bitsPerSample / 8;
            _hdr_chunkSize = 36 + _hdr_subchunk2Size;

            // Go back and write WAV file header
            _writer.Seek(0, SeekOrigin.Begin);            
            _writer.Write(_hdr_chunkID);
            _writer.Write(_hdr_chunkSize);
            _writer.Write(_hdr_format);
            _writer.Write(_hdr_subchunk1ID);
            _writer.Write(_hdr_subchunk1Size);
            _writer.Write(_hdr_audioFormat);
            _writer.Write(_hdr_numChannels);
            _writer.Write(_hdr_sampleRate);
            _writer.Write(_hdr_byteRate);
            _writer.Write(_hdr_blockAlign);
            _writer.Write(_hdr_bitsPerSample);
            _writer.Write(_hdr_subchunk2ID);
            _writer.Write(_hdr_subchunk2Size);            
            _writer.Close();
            //_streamHandle.Free(); // ok, now can be collected
            //_writerHandle.Free(); // ok, now can be collected
            _disposed = true;
        }
        #endregion
    }
}